/* 
 *  Copyright 2012 CodeMagi, Inc.
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.codemagi.servlets.upload;

import com.codemagi.login.AccessException;
import com.codemagi.login.LoginException;
import com.codemagi.login.model.IUser;
import com.codemagi.servlets.*;
import com.codemagi.servlets.model.*;
import com.codemagi.servlets.upload.model.*;
import com.codemagi.servlets.upload.util.*;
import com.codemagi.servlets.validation.*;
import com.codemagi.util.*;
import java.io.*;
import java.util.*;
import java.util.regex.Pattern;
import javax.servlet.*;
import javax.servlet.http.*;
import org.apache.commons.fileupload.disk.*;
import org.apache.log4j.*;
import org.exolab.castor.jdo.CacheManager;
import org.exolab.castor.jdo.Database;
import org.exolab.castor.jdo.OQLQuery;
import org.exolab.castor.jdo.PersistenceException;
import org.exolab.castor.jdo.QueryResults;

/**
 * Class extending the <code>HttpServlet</code> to implement
 * the controller for uploading files.
 * <P>
 * The following actions are supported: upload, modify, select, delete
 * <p>
 * Please see the related documentation for more detailed
 * information on process flow and functionality.
 *
 * @author August Detlefsen for CodeMagi, Inc.
 * @version 1.0
 */
public class UploadController extends com.codemagi.servlets.BaseServlet {

    //set to true to view debug output
    Logger log = Logger.getLogger(this.getClass());

    //views specific to UploadController
    protected String VIEW_MAIN    = "/upload/index.jsp";
    protected String VIEW_UPLOAD  = "/upload/form_upload.jsp";
    protected String VIEW_MODIFY  = "/upload/modify.jsp";
    protected String VIEW_DELETE  = "/upload/delete.jsp";
    protected String VIEW_ERROR   = "/upload/error.jsp";

    //messages specific to upload
    protected static final String MSG_MISSING_FILE     = "No file specified.";
    protected static final String MSG_INVALID_FILE     = "Invalid file specified.";
    protected static final String MSG_INVALID_PARAMS   = "Invalid request parameters.";
    protected static final String MSG_INVALID_CATEGORY = "Invalid category specified.";
    protected static final String MSG_FILE_UPLOADED    = "File uploaded successfully.";
    protected static final String MSG_FILE_UPDATED     = "File updated successfully.";
    protected static final String MSG_FILE_DELETED     = "File deleted successfully.";
    protected static final String MSG_INVALID_PARENT   = "You must select a parent first.";
    protected static final String MSG_FILE_OVERSIZE    = "The uploaded file exceeds the maximum allowable size.";

    //access controls specific to upload
    protected static final String ACCESS_UPLOADER      = "UPLOAD";

    //queries
    public static final String QRY_FILE_CATEGORIES =
        " SELECT file_category_id, file_category_name FROM file_category ORDER BY sort_order ASC ";

    //directory where uploaded files will be stored
    protected static String FILE_SEPARATOR   = System.getProperty("file.separator");  //TODO: make sure this is used throughout

    protected String UPLOAD_DIRECTORY = "/downloads/private/";

    //The parent type for this Uploader
    protected Class UPLOAD_CLASS = com.codemagi.servlets.upload.model.Upload.class;
    protected Class PARENT_CLASS = com.codemagi.servlets.model.Node.class;
    protected Class USER_CLASS   = com.codemagi.login.model.User.class;    //TODO: Is this needed? where is it referenced? 

    /** The number of uploads to allow at once */
    protected Integer MAX_UPLOADS = 1;

    /** The maximum file size to allow */

    /** Whether to allow uploads with no parent */
    protected boolean requireParent = false;

    /**
     * Initializes the servlet when it is loaded by the
     * servlet engine.
     *
     * @param config the configuration as <code>ServletConfig</code>
     *
     * @throws ServletException if initialization fails.
     */
    public void init(ServletConfig config)
	throws ServletException {

	super.init(config);

	try {
	    String parentClass = config.getInitParameter("parentClass");
	    log.debug("Setting PARENT_CLASS to: " + parentClass);
	    if (!Utils.isEmpty(parentClass)) PARENT_CLASS = Class.forName(parentClass);
	} catch (ClassNotFoundException cnfe) {
	    throw new ServletException(cnfe);
	}

	try {
	    String uploadClass = config.getInitParameter("uploadClass");
	    log.debug("Setting UPLOAD_CLASS to: " + uploadClass);
	    if (!Utils.isEmpty(uploadClass)) UPLOAD_CLASS = Class.forName(uploadClass);
	} catch (ClassNotFoundException cnfe) {
	    throw new ServletException(cnfe);
	}

	//set maximum number of uploads at one time
	try {
	    Integer pMaxUploads = convertInteger( config.getInitParameter("maxUploads"), true );
	    if (pMaxUploads != null) MAX_UPLOADS = pMaxUploads;
	} catch (AppException ae) {
	    //no-op
	}

        //load the properties file and set views based on it                
        String propFilename = config.getInitParameter("properties.filename");
        if (!Utils.isEmpty(propFilename)) {
            try {
		Properties properties   = new Properties();
                String propFileLocation = location + WEB_INF_DIR + ETC_DIR + propFilename;
                log.debug("Attempting to load dashboard properties from: " + propFileLocation);
                FileInputStream in      = new FileInputStream(propFileLocation);
                properties.load(in);

                VIEW_MAIN   = Utils.nvl( properties.getProperty("view.main"),   VIEW_MAIN );
                VIEW_UPLOAD = Utils.nvl( properties.getProperty("view.upload"), VIEW_UPLOAD );
                VIEW_MODIFY = Utils.nvl( properties.getProperty("view.modify"), VIEW_MODIFY );
                VIEW_DELETE = Utils.nvl( properties.getProperty("view.delete"), VIEW_DELETE );

            } catch (IOException ioe) {
                throw new ServletException(MSG_PROPERTIES_NOT_FOUND);
            }
        }

        //create validators
        IValidator integerValidator = new IntegerValidator();
        IValidator stripHtmlValidator = new StripHtmlValidator();
        IValidator notNullValidator = new NotNullValidator();
        IValidator booleanValidator = new BooleanValidator();
        IValidator stringLength512Validator = new StringLengthValidator(512);
        IValidator stringLength2048Validator = new StringLengthValidator(2048);
        IValidator[] int_notNull_validators =
            { integerValidator, notNullValidator };
        IValidator[] strip_notNull_validators =
            { stripHtmlValidator, notNullValidator };
        IValidator[] strip_512_validators =
            { stripHtmlValidator, stringLength512Validator };

        //add input validators
        addValidator("moveUp", "returnPage", stripHtmlValidator);
        addValidator("moveUp", "fileId", int_notNull_validators);

        addValidator("moveDown", "returnPage", stripHtmlValidator);
        addValidator("moveDown", "fileId", int_notNull_validators);

        addValidator("select", "returnPage", stripHtmlValidator);
        addValidator("select", "fileId", int_notNull_validators);

        addValidator("get", "fileId", int_notNull_validators);

        addValidator("delete", "returnPage", stripHtmlValidator);
        addValidator("delete", "fileId", int_notNull_validators);

        addValidator("upload", "returnPage", stripHtmlValidator);
        addValidator("upload", "parentId", integerValidator);
	if (requireParent) addValidator("upload", "parentId", notNullValidator);
	addValidator("upload", "categoryId", integerValidator);
	addValidator("upload", "name", strip_notNull_validators);
	addValidator("upload", "name", stringLength512Validator);
	addValidator("upload", "description", stripHtmlValidator);
	addValidator("upload", "description", stringLength2048Validator);
	addValidator("upload", "isPublic", booleanValidator);

	addValidator("multiUpload", "returnPage", stripHtmlValidator);
	addValidator("multiUpload", "parentId", integerValidator);
	if (requireParent) addValidator("multiUpload", "parentId", notNullValidator);
	addValidator("multiUpload", "categoryId", integerValidator);
	addValidator("multiUpload", "name", strip_512_validators);
	addValidator("multiUpload", "description", stripHtmlValidator);
	addValidator("multiUpload", "description", stringLength2048Validator);
	addValidator("multiUpload", "isPublic", booleanValidator);

	addValidator("modify", "returnPage", stripHtmlValidator);
	addValidator("modify", "fileId", int_notNull_validators);
	addValidator("modify", "categoryId", integerValidator);
	addValidator("modify", "name", strip_notNull_validators);
	addValidator("modify", "name", stringLength512Validator);
	addValidator("modify", "description", stripHtmlValidator);
	addValidator("modify", "description", stringLength2048Validator);
	addValidator("modify", "isPublic", booleanValidator);
    }


    /**
     * Handles the incoming requests.
     * <p>
     * This implementation ensures authenticated session
     * existence, retrieves the <code>acton</code>
     * and <code>todo</code> parameters, and dispatches all
     * valid actions to the target dispatchers.
     * <p>
     * The flow of the process is described in the related
     * documentation.
     * <p>
     * Application related errors/exceptions are handled
     * by forwarding the request to an error page, or the actual page
     * in case of an inlined error.
     *
     * @param request   A reference to the actual <code>HttpServletRequest</code> instance.
     * @param response  A reference to the actual <code>HttpServletResponse</code> instance.
     *
     * @throws ServletException if servlet related operations fail.
     * @throws IOException if i/o operations fail.
     */
    public void service(HttpServletRequest request, HttpServletResponse response)
	throws ServletException, IOException {
	
	//next page to forward to
	String nextPage = VIEW_MAIN;

	HttpSession session = request.getSession();
	int originalSessionTimout = session.getMaxInactiveInterval();
	log.debug("originalSessionTimout: " + originalSessionTimout);

	IUser user = null;
	
	try {	
	    //get the user 
	    user = getUser(request);

	    //check that request is within size parameters BEFORE parsing 
	    log.debug("REQUEST LENGTH: " + request.getContentLength());
	    checkRequestSize(request);

	    //deal with multipart
	    String action    = request.getParameter("acton");	
	    Map params = null;
            try {
                params = parseCommonsMultipart(request);
                action = (String)params.get("acton");
            } catch (Exception e) {
		log.debug("Request is NOT multipart/form-data");
                //request might not be multipart
            }

	    log.debug("ACTON: " + action);

	    //first handle actions that do not require login
	    if ("get".equals(action)) {
		nextPage = dispatchGet(request, response);

	    } else {
		
		//next handle actions that do require login
		//make sure user is logged in and has permission to modify files
		checkLogin(request);

		//for big uploads set the session to never timeout 
		session.setMaxInactiveInterval(-1);
			    
		//forward to the appropriate handler method, based on action
		if ("progress".equals(action)) {
		    nextPage = dispatchProgress(request, response);

		} else if ("upload".equals(action)) {
		    if (params == null) {
			//not a submit, foward to edit page
			nextPage = dispatchPreUpload(request, response);
		    } else {
			nextPage = dispatchUpload(request, response, params);
		    }
		
		} else if ("multiUpload".equals(action)) {
		    nextPage = dispatchMultiUpload(request, response, params);
    
		} else if ("moveUp".equals(action)) {
		    nextPage = dispatchMoveUp(request, response);
		    
		} else if ("moveDown".equals(action)) {
		    nextPage = dispatchMoveDown(request, response);

		} else if ("modify".equals(action)) {
		    nextPage = dispatchModify(request, response, params);
		    
		} else if ("select".equals(action)) {
		    nextPage = dispatchSelect(request, response);
		    
		} else if ("delete".equals(action)) {
		    nextPage = dispatchDelete(request, response);
		    
		} 

	    }

	} catch (FileSizeException ae) {
	    request.setAttribute("error_message", ae.getMessage());
	    if (user != null) {
		application.getContext("/").setAttribute("error_message." + user.getSsoHash(), ae.getMessage());
	    }
	    response.sendRedirect(request.getContextPath() + VIEW_ERROR);
	    response.flushBuffer();
	    nextPage = VIEW_NONE;

	} catch (AccessException ae) {
	    nextPage = handleAccessException(request, response, ae);
	    
	} catch (LoginException le) {
	    nextPage = handleLoginException(request, response, le);

	} finally {
	    //reset the session timeout to the normal value
	    session.setMaxInactiveInterval(originalSessionTimout);

	}	    
		
	//dispatch back to where it came from
	log.debug("Acton complete, forarding to: " + nextPage);
	if (!VIEW_NONE.equals(nextPage)) {
	    RequestDispatcher dispatcher = getServletContext().getRequestDispatcher(nextPage);
	    dispatcher.forward(request, response);
	}

	return;
    }


    /**
     * returns an XML object with the upload progress
     */
    protected String dispatchProgress(HttpServletRequest request, HttpServletResponse response) {
	
	log.debug("***** dispatchProgress *****");

	HttpSession session = request.getSession();
	
	UploadInfo info = (UploadInfo)session.getAttribute("uploadInfo");

	try {
	    marshal(info, response, mapping);

	} catch (Exception e) {
	    log.debug("Exception attempting to marshal UploadInfo to response", e);

	}

	return VIEW_NONE;
    }


    /**
     * Dispatches <code>moveUp</code> actions. Modifies a File from persistence layer.                
     */
    protected String dispatchMoveUp(HttpServletRequest request, HttpServletResponse response)
        throws LoginException, AccessException {

        log.debug("***** dispatchMoveUp *****");

        String nextPage = VIEW_MAIN;

        Database db = null;

        try {
	    nextPage       = getReturnPage(request, VIEW_MAIN);
            Integer fileId = getInteger("fileId", request);

	    checkValidation(request);

            //get the DB instance
            db = getJDODatabase();
            db.setAutoStore(true);
            db.begin();

            //get the file to update from DB
            Upload modFile = (Upload)db.load(UPLOAD_CLASS, fileId);

            //make user has permission to edit this file
            checkPermission(request, modFile);

            db.rollback(); //make sure there is no confusion in the transaction
            db.begin();

            OQLQuery oql = null;
	    oql = db.getOQLQuery(" SELECT u FROM " + UPLOAD_CLASS.getName() + " u " +
				 " WHERE u.parent.id = $1 " +
				 " ORDER BY u.sortOrder ASC ");
	    oql.bind(modFile.getParentId());

	    log.debug("Executing no-category OQL");
            QueryResults results = oql.execute();
            Upload file1 = null;
            Upload file2 = null;
            while (results.hasMore()) {
                file2 = (Upload)results.next();
		log.debug("OQL RESULT: " + file2);

                if (fileId.equals(file2.getId())) {
                    if (file1 == null) break;      //file1 is already first
                    break;
                }

                file1 = file2;
            }

            //commit updated bean to DB
            db.commit();
            db.close();

            //success!
            request.setAttribute("error_message", "File updated successfully");

        } catch (AppException se) {
            request.setAttribute("error_message", se.getMessage());

            nextPage = VIEW_MAIN;

        } catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);

            nextPage = VIEW_MODIFY;

        } finally {
            closeJDODatabase(db);

        }

        return nextPage;
    }


    /**
     * Dispatches <code>moveDown</code> actions. Modifies a File from persistence layer.                            
     */
    protected String dispatchMoveDown(HttpServletRequest request, HttpServletResponse response)
        throws LoginException, AccessException {

        log.debug("***** dispatchMoveDown *****");

        String nextPage = VIEW_MAIN;

        Database db = null;

        try {
	    nextPage       = getReturnPage(request, VIEW_MAIN);
            Integer fileId = getInteger("fileId", request);

	    checkValidation(request);

            //get the DB instance
            db = getJDODatabase();
            db.setAutoStore(true);
            db.begin();

            //get the file to update from DB
            Upload modFile = (Upload)db.load(UPLOAD_CLASS, fileId);

            //make user has permission to edit this file
            checkPermission(request, modFile);

            db.rollback(); //make sure there is no confusion in the transaction
            db.begin();

            OQLQuery oql = null;
	    oql = db.getOQLQuery(" SELECT u FROM " + UPLOAD_CLASS.getName() + " u " +
				 " WHERE u.parent.id = $1 " +
				 " ORDER BY u.sortOrder ASC ");
	    oql.bind(modFile.getParentId());

	    log.debug("Executing no-category OQL");

            QueryResults results = oql.execute();
            Upload file1 = null;
            Upload file2 = null;
            while (results.hasMore()) {
                file2 = (Upload)results.next();
		log.debug("OQL RESULT: " + file2);

                if (file1 != null && fileId.equals(file1.getId())) {
                    break;
                }

                file1 = file2;
            }

            //commit updated bean to DB
            db.commit();
            db.close();

            //success!
            request.setAttribute("error_message", "File updated successfully");

        } catch (AppException se) {
            request.setAttribute("error_message", se.getMessage());

            nextPage = VIEW_MAIN;

        } catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);

            nextPage = VIEW_MODIFY;

        } finally {
            closeJDODatabase(db);

        }

        return nextPage;
    }


    /**
     * Dispatches <code>select</code> actions. Loads the specified File into session, forwards to edit page.
     * @deprecated  use NavigationController instead
     */
    protected String dispatchSelect(HttpServletRequest request, HttpServletResponse response)
        throws LoginException, AccessException {

	String nextPage = VIEW_MAIN;

	HttpSession session = request.getSession();

	Database db = null;

        try {
	    //get request params
	    Integer fileId = getInteger("fileId", request);
	    String todo    = request.getParameter("todo");

	    nextPage = getReturnPage(request, VIEW_MAIN);

	    checkValidation(request);

	    db = getJDODatabase();
            db.begin();

	    Upload file = (Upload)db.load(UPLOAD_CLASS, fileId);

	    //make user has permission to edit this file
            checkPermission(request, file);

	    session.setAttribute("file", file);
	    request.setAttribute("file", file);
	    request.setAttribute("parent", file.getParent());

	    //determine next page
	    if ("delete".equals(todo)) {
		nextPage = VIEW_DELETE;
	    } else if ("modify".equals(todo)) {
		nextPage = VIEW_MODIFY;
	    } else if ("modifyInPlace".equals(todo)) {
		request.setAttribute("modifyInPlace", "true");
	    }

	} catch (AppException sne) {
            log.debug("", sne);
            request.setAttribute("error_message", sne.getMessage());

	    nextPage = VIEW_MAIN;

	} catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);

	} finally {
	    closeJDODatabase(db);

        }

	return nextPage;
    }


    /**
     * Dispatches <code>get</code> actions. Forwards directly to the specified file
     */
    protected String dispatchGet(HttpServletRequest request, HttpServletResponse response)
        throws LoginException, AccessException {

	//next page if the file does not exist
	String nextPage = VIEW_MAIN;

	HttpSession session = request.getSession();

	Database db = null;

        try {

	    //get request params
	    Integer fileId = getInteger("fileId", request);

	    checkValidation(request);

	    db = getJDODatabase();
            db.begin();

	    Upload file = (Upload)db.load(UPLOAD_CLASS, fileId);

	    //make sure we loaded a valid file
	    if (file == null) throw new AppException(MSG_INVALID_FILE);

	    db.rollback();
	    db.close();

	    //success! Send user to file
	    nextPage = file.getLink();

	} catch (AppException sne) {
            log.debug("", sne);
            request.setAttribute("error_message", sne.getMessage());

	} catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);

	} finally {
	    closeJDODatabase(db);

        }

	return nextPage;
    }


    /**
     * Dispatches <code>delete</code> actions. Loads the specified File into session, forwards to edit page.
     */
    protected String dispatchDelete(HttpServletRequest request, HttpServletResponse response)
        throws LoginException, AccessException {

	String nextPage = VIEW_MAIN;

	Database db = null;

        try {
	    IUser user = checkLogin(request);

	    //get request params
	    nextPage       = getReturnPage(request, VIEW_MAIN);
	    Integer fileId = getInteger("fileId", request);

	    checkValidation(request);

	    db = getJDODatabase();
            db.begin();

	    Upload file = (Upload)db.load(UPLOAD_CLASS, fileId);

	    //make sure user has access to delete this file
	    checkDeletePermission(request, file);

	    //make sure we loaded a valid file
	    if (file == null) throw new AppException(MSG_INVALID_FILE);

	    //actually delete the file from the DB!
            db.remove(file);
	    db.commit();

            ServletContext rootContext = getServletContext().getContext("/");
            String rootPath            = rootContext.getRealPath("");

	    //delete the file from the filesystem
	    //File removeFile = new File(location + file.getLink());
	    File removeFile = new File(rootPath + file.getLink());
	    removeFile.delete();

	    //success!
	    request.getSession().setAttribute("file", null);
	    request.setAttribute("error_message", MSG_FILE_DELETED);

	    //execute the after delete hook
	    doAfterDelete(file, request);
	    
	} catch (AppException sne) {
            log.debug("", sne);
            request.setAttribute("error_message", sne.getMessage());

	    nextPage = VIEW_DELETE;

	} catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);

	    nextPage = VIEW_MAIN;

	} finally {
	    closeJDODatabase(db);
        }

	return nextPage;
    }


    /**
     * Dispatches pre-upload actions -ie: upload without a submit. Loads the parent object 
     * into request and forwards to the edit page. 
     */
    protected String dispatchPreUpload(HttpServletRequest request, HttpServletResponse response) 
	throws LoginException, AccessException {

	log.debug("***** dispatchPreUpload *****");

	String nextPage = VIEW_UPLOAD;

	String returnPage = VIEW_MAIN;

	Database db = null;
	
	try {
	    returnPage = getReturnPage(request, VIEW_MAIN);
	    log.debug("1");
	    Integer parentId = getInteger("parentId", request);

	    checkValidation(request);
	    
	    //persist
	    db = getJDODatabase();
	    db.begin();

	    log.debug("2");
	    Node parent = (Node)db.load(PARENT_CLASS, parentId, Database.ReadOnly);

	    db.rollback();

	    //create a new Upload and place it in the request for use by the form page
	    Upload upload = (Upload)UPLOAD_CLASS.newInstance();
	    //	    upload.setParent(parent);
	    parent.addChild(upload);
	    request.setAttribute("file", upload);

	    log.debug("3");
	    request.setAttribute("parent", parent);

	} catch (AppException sne) {
	    log.debug("", sne);
	    request.setAttribute("error_message", sne.getMessage());

	    nextPage = returnPage;

	} catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);

	    nextPage = returnPage;

	} catch (InstantiationException ie) {
	    //thrown by Class.newInstance()
	    log.debug("", ie);
	    request.setAttribute("error_message", MSG_RUNTIME_ERROR);

            nextPage = returnPage;

	} catch (IllegalAccessException ie) {
	    //thrown by Class.newInstance()
	    log.debug("", ie);
            request.setAttribute("error_message", MSG_RUNTIME_ERROR);

            nextPage = returnPage;

	} finally {
	    closeJDODatabase(db);

        }

	return nextPage;
    }


    /**
     * Dispatches <code>upload</code> actions. Creates, updates, or removes 
     * from persistence layer depending on request content.
     */
    protected String dispatchUpload(HttpServletRequest request, HttpServletResponse response, Map params) 
	throws LoginException, AccessException {

	log.debug("***** dispatchUpload *****");

	//name variables that need to be available in full method scope
	String nextPage                 = VIEW_MAIN;
	Database db                     = null;
	InputStream fileIn              = null;
	FileOutputStream fileOut        = null;

	try {
	    //get request params
	    if (params == null) throw new AppException(MSG_INVALID_PARAMS);

	    nextPage = getReturnPage(request, VIEW_MAIN, params);
	    Integer parentId = getInteger("parentId", request, params);
	    //if (requireParent) checkRequired(parentId);

	    //do the DB transaction
	    db = getJDODatabase();
	    db.begin();

	    //create the new upload 
	    Upload upload = (Upload)UPLOAD_CLASS.newInstance();

	    //load the parent of this upload
	    Node parent = null;
	    if (parentId != null) {
		parent = (Node)db.load(PARENT_CLASS, parentId);
		if (requireParent && parent == null) throw new AppException(MSG_INVALID_PARENT);
		log.debug("PARENT: " + parent);

		//set the parent into the upload
                parent.addChild(upload);
	    }

	    //set parent into request in case we get any errors
	    request.setAttribute("parent", parent);

	    upload.setParent(parent);  //HACK! Need to have the parent for the checkPermission() call

	    //check whether the user has permission to upload
	    checkPermission(request, upload);

	    //set the upload into the request for use by the form page if there is an error
	    request.setAttribute("file", upload);

	    Integer categoryId  = getInteger("categoryId", request, params);
	    String name         = getString("name", request, params);
	    String desc         = getString("description", request, params);
	    Boolean isPublic    = getBoolean("isPublic", request, params);

	    checkValidation(request);

	    //get the actual file, run checks
	    DiskFileItem file   = (DiskFileItem)params.get("attachment");
	    if (file == null)       throw new AppException(MSG_MISSING_FILE);
	    if (file.isFormField()) throw new AppException(MSG_INVALID_FILE);

	    long fileSize       = file.getSize();
	    String filename     = file.getName();

	    log.debug("FILE: " + filename );
	    log.debug("FILE SIZE: " + fileSize);
	    log.debug("PUBLIC: " + isPublic);

	    //check that the file is not too large
	    checkFileSize(request, fileSize);

	    Object[] required = { filename };  // "name" is in validators
	    checkRequired( required );

	    //if there is no name entered, use the filename
	    //subclasses can remove the NotNullValidator on the name param
	    if (Utils.isEmpty(name)) name = filename;

	    //remove any invalid chars in the filename
	    filename = warpFilename(filename);

	    //create the link name
	    String dir       = UPLOAD_DIRECTORY + UIDGenerator.getUID() + FILE_SEPARATOR;
	    String link      = dir + filename;
	    
	    log.debug("LINK: " + link);
	
	    //get a file type that corresponds with the extension
	    FileType type = getFileTypeByExtension(filename, db);

	    //the new file goes at the end of its category if a category is set, 
	    //otherwise at the end of its parent
	    //upload.setSortOrder( getLastSortOrder(db, parentId, categoryId) );

	    //set values into bean	    
	    upload.setFileType(type);
	    upload.setName(name);
	    upload.setLink(link);
	    upload.setDescription(desc);
	    upload.setPublic(isPublic);
	    upload.setSize(fileSize);

	    //Call core functions
	    setNodeFields(upload, request, db);  //saves new upload in DB
	    TaxonomyController.setTerms(upload, params, db);

	    //get the location to write the file
	    ServletContext rootContext = getServletContext().getContext("/");
	    String rootPath            = rootContext.getRealPath("");

	    //create the directory if needed
	    java.io.File outDir = new File(rootPath + dir);
	    outDir.mkdirs();
	    
	    log.debug("Writing file to outDir: " + rootPath + dir);

	    //save the file to the filesystem
	    fileIn  = file.getInputStream();
	    fileOut = new FileOutputStream(rootPath + link);
	    log.debug("Streaming file from: " + fileIn + " to: " + fileOut);
	    StreamUtils.readWrite(fileIn, fileOut, 1024 * 32); //32K buffer

	    db.commit();

	    //clear the cache for the parent object
	    if (parentId != null) {
		CacheManager cm = db.getCacheManager();
		cm.expireCache(PARENT_CLASS, parentId);
	    }

	    request.setAttribute("error_message", "File uploaded successfully");

	    //execute the after upload hook
	    doAfterUpload(upload, request, params);
	    
	} catch (AppException sne) {
	    log.debug("", sne);
	    request.setAttribute("error_message", sne.getMessage());

	    nextPage = VIEW_UPLOAD;

	} catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);

	    nextPage = VIEW_UPLOAD;

	} catch (IOException ioe) {
	    log.debug("", ioe);
            request.setAttribute("error_message", MSG_IO_ERROR);

            nextPage = VIEW_UPLOAD;

	} catch (InstantiationException ie) {
	    //thrown by Class.newInstance()
	    log.debug("", ie);
	    request.setAttribute("error_message", MSG_RUNTIME_ERROR);

            nextPage = VIEW_UPLOAD;

	} catch (IllegalAccessException ie) {
	    //thrown by Class.newInstance()
	    log.debug("", ie);
            request.setAttribute("error_message", MSG_RUNTIME_ERROR);

            nextPage = VIEW_UPLOAD;

	} finally {
	    closeJDODatabase(db);

	    //close output streams to release memory resources
	    closeStream(fileIn);
	    closeStream(fileOut);
        }

	return nextPage;
    }
    
    
    /**
     * Dispatches <code>multiUpload</code> actions. Creates, updates, or removes 
     * from persistence layer depending on request content.
     */
    protected String dispatchMultiUpload(HttpServletRequest request, HttpServletResponse response, Map params) 
	throws LoginException, AccessException {

	log.debug("***** dispatchMultiUpload *****");

	//name variables that need to be available in full method scope
	String nextPage                = VIEW_MAIN;
	Database db                    = null;
	InputStream fileIn             = null;
	FileOutputStream fileOut       = null;

	try {
	    //get request params
	    if (params == null) throw new AppException(MSG_INVALID_PARAMS);

	    nextPage = getReturnPage(request, VIEW_MAIN, params);
	    Integer parentId = getInteger("parentId", request, params);
	    //	    if (requireParent) checkRequired(parentId);

	    //do the DB transaction
	    db = getJDODatabase();
	    db.begin();

	    //create the new upload 
	    Upload upload = (Upload)UPLOAD_CLASS.newInstance();

	    //load the parent of this upload
	    Node parent = null;
	    if (parentId != null) {
		parent = (Node)db.load(PARENT_CLASS, parentId);
		if (requireParent && parent == null) throw new AppException(MSG_INVALID_PARENT);
		log.debug("PARENT: " + parent);

		//set the parent into the upload
                parent.addChild(upload);
	    }

	    //set parent into request in case we get any errors
	    request.setAttribute("parent", parent);

	    upload.setParent(parent);  //HACK! Need to have the parent for the checkPermission() call

	    //check whether the user has permission to upload
	    checkPermission(request, upload);

	    //set the upload into the request for use by the form page if there is an error
	    request.setAttribute("file", upload);

	    Integer categoryId  = getInteger("categoryId", request, params);
	    String desc         = getString("description", request, params);
	    Boolean isPublic    = getBoolean("isPublic", request, params);

	    List<Upload> uploads = new ArrayList<Upload>(MAX_UPLOADS);

	    for (int i = 1; i <= MAX_UPLOADS; i++) {

		//get the actual file, run checks
		DiskFileItem file   = (DiskFileItem)params.get("attachment" + i);
		if (file == null) continue;
		if (file.isFormField()) throw new AppException(MSG_INVALID_FILE);
		
		if (i > 1) {
		    //create a new upload
		    upload = (Upload)UPLOAD_CLASS.newInstance();

		    if (parent != null) {
			//set the parent into the upload
			parent.addChild(upload);
			upload.setParent(parent);
		    }
		}

		String name         = getString("name", i, request, params);
		String itemDesc     = getString("description", i, request, params);
		log.debug("itemDesc: " + itemDesc);

		checkValidation(request);

		long fileSize       = file.getSize();
		String filename     = file.getName();
		
		log.debug("FILE: " + filename );
		log.debug("FILE SIZE: " + fileSize);
		log.debug("PUBLIC: " + isPublic);
		
		//check that the file is not too large
		checkFileSize(request, fileSize);

		String[] required = { filename };   // "name" is in validators
		checkRequired( required );
		
		//remove any invalid chars in the filename
		filename = warpFilename(filename);
		
		//create the link name
		String dir       = UPLOAD_DIRECTORY + UIDGenerator.getUID() + FILE_SEPARATOR;
		String link      = dir + filename;
	    
		log.debug("LINK: " + link);
		
		//get a file type that corresponds with the extension
		FileType type = getFileTypeByExtension(filename, db);
		
		//the new file goes at the end of its category if a category is set, 
		//otherwise at the end of its parent
		//upload.setSortOrder( getLastSortOrder(db, parentId, categoryId) );

		//set values into bean	    
		upload.setFileType(type);
		upload.setName(name);
		upload.setLink(link);
		upload.setDescription( Utils.nvl(itemDesc, desc) );
		upload.setPublic(isPublic);
		upload.setSize(fileSize);
		
		//Call core functions
		setNodeFields(upload, request, db);  //saves new upload in DB
		TaxonomyController.setTerms(upload, request, db);

		uploads.add(upload);
	    
		//get the location to write the file
		ServletContext rootContext = getServletContext().getContext("/");
		String rootPath            = rootContext.getRealPath("");
		
		//create the directory if needed
		java.io.File outDir = new File(rootPath + dir);
		outDir.mkdirs();
		
		log.debug("Writing file to outDir: " + rootPath + dir);
		
		//save the file to the filesystem
		fileIn  = file.getInputStream();
		fileOut = new FileOutputStream(rootPath + link);
		log.debug("Streaming file from: " + fileIn + " to: " + fileOut);
		StreamUtils.readWrite(fileIn, fileOut, 1024 * 32); //32K buffer
		
	    } //END for MAX_UPLOADS

	    db.commit();

	    //clear the cache for the parent object
	    if (parentId != null) {
		CacheManager cm = db.getCacheManager();
		cm.expireCache(PARENT_CLASS, parentId);
	    }

	    request.setAttribute("error_message", "File uploaded successfully");

	    //execute the after upload hook
	    doAfterUpload(uploads, request, params);
	    
	} catch (AppException sne) {
	    log.debug("", sne);
	    request.setAttribute("error_message", sne.getMessage());

	    nextPage = VIEW_UPLOAD;

	} catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);

	    nextPage = VIEW_UPLOAD;

	} catch (IOException ioe) {
	    log.debug("", ioe);
            request.setAttribute("error_message", MSG_IO_ERROR);

            nextPage = VIEW_UPLOAD;

	} catch (InstantiationException ie) {
	    //thrown by Class.newInstance()
	    log.debug("", ie);
	    request.setAttribute("error_message", MSG_RUNTIME_ERROR);

            nextPage = VIEW_UPLOAD;

	} catch (IllegalAccessException ie) {
	    //thrown by Class.newInstance()
	    log.debug("", ie);
            request.setAttribute("error_message", MSG_RUNTIME_ERROR);

            nextPage = VIEW_UPLOAD;

	} finally {
	    closeJDODatabase(db);

	    //close output streams to release memory resources
	    closeStream(fileIn);
	    closeStream(fileOut);
        }

	return nextPage;
    }
    
    
    /**
     * Dispatches <code>modify</code> actions. 
     */
    protected String dispatchModify(HttpServletRequest request, HttpServletResponse response, Map params) 
	throws LoginException, AccessException {

	log.debug("***** dispatchModify *****");

	HttpSession session = request.getSession();
	
	//name variables that need to be available in full method scope
	String nextPage                 = VIEW_MAIN;
	Database db                     = null;
	InputStream fileIn              = null;
	FileOutputStream fileOut        = null;

	try {
	    //get request params
	    if (params == null) throw new AppException(MSG_INVALID_PARAMS);

	    nextPage           = getReturnPage(request, VIEW_MAIN, params);
	    Integer fileId     = getInteger("fileId", request, params);
	    //checkRequired(fileId);   // in validators now

	    //do the DB transaction
            db = getJDODatabase();
            db.begin();

	    //get the item to update from DB
	    Upload upload = (Upload)db.load(UPLOAD_CLASS, fileId);
	    log.debug("UPLOAD: " + upload + " PARENT ID: " + upload.getParentId());

	    //place upload into request for use by the form page
	    request.setAttribute("file", upload);

            //make user has permission to edit this file
            checkPermission(request, upload);

	    //if the user entered a new file, set new info in bean
	    Integer categoryId = getInteger("categoryId", request, params);
	    String name        = getString("name", request, params);
	    String desc        = getString("description", request, params);
	    Boolean isPublic   = getBoolean("isPublic", request, params);

	    checkValidation(request);

	    //get the actual file, run checks
	    DiskFileItem file  = (DiskFileItem)params.get("attachment");
	    long fileSize      = -1l;
	    String filename    = "";
	    if (file != null) {
		fileSize       = file.getSize();
		filename       = file.getName();
	    }

	    log.debug("FILE: " + filename + " = " + file);
	    log.debug("FILE SIZE: " + fileSize);
	    log.debug("PUBLIC: " + isPublic);
	    log.debug("CAT ID: " + categoryId);

	    //check that the file is not too large
	    checkFileSize(request, fileSize);

	    if ( !Utils.isEmpty(filename) ) {
		//remove any spaces in the filename
		filename = warpFilename(filename);
		
		//create the NEW link name
		String prevLink = upload.getLink();
		String dir  = prevLink.substring(0, prevLink.lastIndexOf("/") + 1);
		String link = dir + filename;
		
		log.debug("LINK: " + link);
		
		//get a file type that corresponds with the extension
		FileType type = getFileTypeByExtension(filename, db);
	    
		//set new file info in bean
		upload.setLink(link);
		upload.setFileType(type);
		upload.setFileDate(new java.util.Date());
		upload.setSize(fileSize);

		//get the location to write the file
		ServletContext rootContext = getServletContext().getContext("/");
		String rootPath            = rootContext.getRealPath("");

		//create the directory if needed
		java.io.File outDir = new File(rootPath + dir);
		outDir.mkdirs();
		
		log.debug("Writing file to outDir: " + rootPath + dir);

		//save the file to the filesystem  
		fileIn  = file.getInputStream();
		File outFile = new File(rootPath + link);
		fileOut = new FileOutputStream(outFile);
		log.debug("Streaming file from: " + fileIn + " to: " + fileOut);
		StreamUtils.readWrite(fileIn, fileOut, 1024 * 32); //32K buffer

		//delete the old file from the filesystem
		File removeFile = new File(rootPath + prevLink);
		if ( !removeFile.equals(outFile) ) removeFile.delete();

	    }
	    
	    //set additional info in bean
	    upload.setName(name);
	    upload.setDescription(desc);
	    upload.setPublic(isPublic);
	    
	    //commit updated bean to DB
	    db.commit();
	    
	    //success!
	    request.setAttribute("error_message", "File updated successfully");
	    session.setAttribute("file", null);
	    
        } catch (AppException ae) {
	    log.debug("", ae);
            request.setAttribute("error_message", ae.getMessage());

	    nextPage = VIEW_MODIFY;

	} catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);

	    nextPage = VIEW_MODIFY;

	} catch (IOException ioe) {
	    log.debug("", ioe);
            request.setAttribute("error_message", MSG_IO_ERROR);

            nextPage = VIEW_MODIFY;

	} finally {
	    closeJDODatabase(db);

	    //close output streams to release memory resources
            closeStream(fileIn);
            closeStream(fileOut);

        }
	
	return nextPage;
    }


    //HOOKS -------------------------------------------------------

    /**
     * Called to execute any actions that need to happen after dispatchUpload completes successfully.
     *
     * Extend this method to implement custom functionality (such as emailing admins, etc)
     *
     * @deprecated    Use the three argument version instead
     */
    protected void doAfterUpload(Upload upload, HttpServletRequest request) {
	doAfterUpload(upload, request, new HashMap());
    }


    /**
     * Called to execute any actions that need to happen after dispatchUpload completes successfully.
     *
     * Extend this method to implement custom functionality (such as emailing admins, etc)
     */
    protected void doAfterUpload(Upload upload, HttpServletRequest request, Map params) {
	//no-op
    }


    /**
     * Called to execute any actions that need to happen after dispatchUpload completes successfully.
     *
     * Extend this method to implement custom functionality (such as emailing admins, etc)
     *
     * @deprecated    Use the three argument version instead
     */
    protected void doAfterUpload(List<Upload> uploads, HttpServletRequest request) {
	doAfterUpload(uploads, request, new HashMap());
    }


    /**
     * Called to execute any actions that need to happen after dispatchUpload completes successfully.
     *
     * Extend this method to implement custom functionality (such as emailing admins, etc)
     */
    protected void doAfterUpload(List<Upload> uploads, HttpServletRequest request, Map params) {
	//no-op
    }


    /**
     * Called to execute any actions that need to happen after dispatchDelete completes successfully.
     *
     * Extend this method to implement custom functionality (such as clearing search lists, etc)
     */
    protected void doAfterDelete(Upload upload, HttpServletRequest request) {
	//no-op
    }


    //UTILITY METHODS ---------------------------------------------

    /**
     * Checks that a user has permission to modify uploads in a particular Parent
     */
    protected void checkPermission(HttpServletRequest request) throws AppException, AccessException {
	
    }


    /**
     * Checks that a user has permission to modify uploads in a particular Parent
     */
    protected void checkPermission(HttpServletRequest request, Upload upload) 
	throws AppException, LoginException, AccessException {
	
    }


    /**
     * Checks that an IUser has permission to delete the passed Upload
     */
    protected void checkDeletePermission(HttpServletRequest request, Upload upload) 
	throws LoginException, AppException, AccessException {

	checkPermission(request, ACCESS_SUPERUSER);
    }


    /**
     * Checks that the file is within size parameters. 
     *
     * Default maximum file size is 64MB. Extend this method in subclasses to implement application-specific behavior.
     */
    protected void checkFileSize(HttpServletRequest request, long fileSize) 
	throws AppException, LoginException {
	
	if (fileSize > MAX_UPLOAD_SIZE) throw new AppException(MSG_FILE_OVERSIZE);

    }


    /**
     * Checks that the request size is within size parameters. 
     *
     * Default maximum file size is 64MB. Extend this method in subclasses to implement application-specific behavior.
     */
    protected void checkRequestSize(HttpServletRequest request) throws FileSizeException {
	
	if (request.getContentLength() > MAX_UPLOAD_SIZE) throw new FileSizeException(MSG_FILE_OVERSIZE + " The maximum file size is " + FileUtils.getFileSizeString(MAX_UPLOAD_SIZE) + ".");

    }


    /**
     * Returns a FileType based on an extension
     */
    protected FileType getFileTypeByExtension(String filename, Database db) 
	throws org.exolab.castor.jdo.PersistenceException {

	log.debug("getFileTypeByExtension( " + filename + " )");

	String extension = FileUtils.getExtension(filename).toLowerCase();

	FileType type = null;
	//get Obj from DB
	OQLQuery oql = db.getOQLQuery(" SELECT t FROM com.codemagi.servlets.upload.model.FileType t " +
				      " WHERE t.extension = $1 ");
	oql.bind(extension);
	QueryResults results = oql.execute();

	if ( results.hasMore() ) {
	    //item already exists in DB
	    type = (FileType)results.next();

	    log.debug("GOT TYPE FROM OQL");

	} else {
	    //unknown type
	    type = (FileType)db.load(FileType.class, new Integer(0));

	}

	return type;
    }


    /**
     * Removes any invalid chars in a filename: 
     * 
     * Removes Windows paths from filename. 
     * 
     * <code>
     * " " -> "_"
     * "#" -> "_"
     * </code>
     *
     * Renames files ending in <code>.jsp</code> to <code>.jsp.src</code>
     */
    protected String warpFilename(String originalName) {

	//remove windows-style paths (submitted by Internet Explorer)
	String output = WINDOWS_PATH_PATTERN.matcher(originalName).replaceAll("");

	//replace spaces and other bad chars
	output = StringUtils.replace(output, " ", "_");
	output = StringUtils.replace(output, "#", "_");
	output = StringUtils.replace(output, "&", "_");
	output = StringUtils.replace(output, ";", "_");	
	output = StringUtils.replace(output, "?", "_");	

	//shorten it up
	output = StringUtils.replace(output, "__", "_");

	//do not allow uploading JSP files! 
	if (output.endsWith(".jsp") || output.endsWith(".jspx")) output = output + ".src";

	//do not allow uploading WAR files! 
	if (output.endsWith(".war")) {
	    log.debug("FILENAME ENDS WITH .war !");
	    output = output.replaceAll("\\.war\\z", ".zip");
	    log.debug("    CHANGED TO: " + output);
	}

	return output;
    }
    
    /**
     * Regular expression for cleaning windows filenames
     */
    static final Pattern WINDOWS_PATH_PATTERN = Pattern.compile(".*\\\\", Pattern.CASE_INSENSITIVE);

}
